﻿#region License
/*
Copyright 2024 Dmitrii Evdokimov
Open source software

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// The most up to date original version by Gérald Barré is available 
// on GitHub: https://github.com/meziantou/Meziantou.Framework/tree/master/src/Meziantou.Framework.Win32.CredentialManager
// NuGet package: https://www.nuget.org/packages/Meziantou.Framework.Win32.CredentialManager/
#endregion

namespace Lib
{
    using System;
    using System.ComponentModel;
    using System.Runtime.InteropServices;
    using System.Text;

    using Microsoft.Win32.SafeHandles;

    using static NativeMethods;

    public static class CredentialManager
    {
        public static Credential ReadCredential(string targetName)
        {
            if (targetName.Contains("*"))
            {
                if (CredEnumerate(targetName, 0, out int count, out IntPtr pCredentials))
                {
                    if (count > 1)
                        throw new Exception($"Windows Credential Manager has more '{targetName}' entries ({count}).");

                    IntPtr credential = Marshal.ReadIntPtr(pCredentials, 0);
                    var cred = Marshal.PtrToStructure(credential, typeof(NativeCredential));
                    return ReadFromNativeCredential((NativeCredential)cred);
                }

                throw new Exception($"Windows Credential Manager has no '{targetName}' entries.");
            }

            if (CredRead(targetName, CredentialType.Generic, 0, out IntPtr nCredPtr))
            {
                using (CriticalCredentialHandle critCred = new CriticalCredentialHandle(nCredPtr))
                {
                    var cred = critCred.GetNativeCredential();
                    return ReadFromNativeCredential(cred);
                }
            }

            throw new Exception($"Windows Credential Manager has no '{targetName}' entries.");
        }

        public static void WriteCredential(string targetName, string userName, string secret)
        {
            NativeCredential credential = new NativeCredential()
            {
                AttributeCount = 0,
                Attributes = IntPtr.Zero,
                Comment = IntPtr.Zero,
                TargetAlias = IntPtr.Zero,
                Type = CredentialType.Generic,
                Persist = (uint)CredentialPersistence.LocalMachine,
                CredentialBlobSize = 0,
                TargetName = Marshal.StringToCoTaskMemUni(targetName),
                CredentialBlob = Marshal.StringToCoTaskMemUni(secret),
                UserName = Marshal.StringToCoTaskMemUni(userName ?? Environment.UserName)
            };

            if (secret != null)
            {
                byte[] byteArray = Encoding.Unicode.GetBytes(secret);

                if (byteArray.Length > 512 * 5)
                    throw new ArgumentOutOfRangeException(nameof(secret), "The secret message has exceeded 2560 bytes.");

                credential.CredentialBlobSize = (uint)byteArray.Length;
            }

            bool written = CredWrite(ref credential, 0);

            Marshal.FreeCoTaskMem(credential.TargetName);
            Marshal.FreeCoTaskMem(credential.CredentialBlob);
            Marshal.FreeCoTaskMem(credential.UserName);

            if (!written)
            {
                int lastError = Marshal.GetLastWin32Error();
                throw new Exception(string.Format("CredWrite failed with the error code {0}.", lastError));
            }
        }

        public static Credential[] EnumerateCrendentials(string filter = null)
        {
            if (CredEnumerate(filter, 0, out int count, out IntPtr pCredentials))
            {
                Credential[] result = new Credential[count];

                for (int n = 0; n < count; n++)
                {
                    IntPtr credential = Marshal.ReadIntPtr(pCredentials, n * Marshal.SizeOf(typeof(IntPtr)));
                    var cred = Marshal.PtrToStructure(credential, typeof(NativeCredential));
                    result[n] = ReadFromNativeCredential((NativeCredential)cred);
                }

                return result;
            }

            int lastError = Marshal.GetLastWin32Error();
            throw new Win32Exception(lastError);
        }

        private enum CredentialPersistence : uint
        {
            Session = 1,
            LocalMachine,
            Enterprise
        }

        private static Credential ReadFromNativeCredential(NativeCredential credential)
        {
            string targetName = Marshal.PtrToStringUni(credential.TargetName);
            string userName = Marshal.PtrToStringUni(credential.UserName);
            string secret = null;

            if (credential.CredentialBlob != IntPtr.Zero)
            {
                secret = Marshal.PtrToStringUni(credential.CredentialBlob, (int)credential.CredentialBlobSize / 2);
            }

            return new Credential(credential.Type, targetName, userName, secret);
        }

        sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
        {
            public CriticalCredentialHandle(IntPtr preexistingHandle)
            {
                SetHandle(preexistingHandle);
            }

            public NativeCredential GetNativeCredential()
            {
                if (!IsInvalid)
                {
                    var cred = Marshal.PtrToStructure(handle, typeof(NativeCredential));
                    return (NativeCredential)cred;
                }

                throw new InvalidOperationException("Invalid CriticalHandle!");
            }

            protected override bool ReleaseHandle()
            {
                if (!IsInvalid)
                {
                    CredFree(handle);
                    SetHandleAsInvalid();

                    return true;
                }

                return false;
            }
        }
    }
}
